{$CLEO .s}
{$USE CLEO+}

SCRIPT_NAME 'OILDROP'

//***********************************************************************************//
///////////////////////////////////////////////////////////////////////////////////////
// Oil Puddle Drop
// Author: AlvarynGTA
///////////////////////////////////////////////////////////////////////////////////////
//
// Streamed oil puddle instance created by OilCore.
//
// Represents one runtime oil puddle.
// Handles rendering, growth, ignition, propagation, burning effects and cleanup.
//
// Shared systems received from OilCore:
// - dropsList: active puddle registry for propagation / validation
// - igniteBufferPtr: shared external ignition XYZ buffer
// - activeDropsPtr: global active puddle counter
// - oilConfigPtr: shared read-only configuration
//
// Runtime model:
// Each puddle runs as its own streamed custom script instance.
// Local state is isolated, while shared systems coordinate global behavior.
//
// State machine:
// STATE_IDLE         = waiting for ignition
// STATE_PENDING_FIRE = ignition detected, waiting propagation delay
// STATE_BURNING      = active fire behavior / visual fading
// STATE_EXPIRED      = no longer ignitable, visual fade only
// STATE_CLEANUP      = unregister and terminate
//
// ownerChar:
// Ped handle used for native fire ownership attribution (damage / wanted logic).
// Use -1 for non-ped or ambient oil sources.
//
// Performance notes:
// - list validation runs periodically
// - propagation checks one puddle per tick
// - ped/object scans run only while burning
// - total cost scales with active puddle count
//
//***********************************************************************************//

CONST
    // -------------------------------------------------------------------------
    // State machine
    // Used by: OilPuddleDrop
    // -------------------------------------------------------------------------
    STATE_IDLE = 0
    STATE_PENDING_FIRE = 1
    STATE_BURNING = 2
    STATE_CLEANUP = 3
    STATE_EXPIRED = 4

    // -------------------------------------------------------------------------
    // Game object model IDs
    // Used by: OilPuddleDrop PROCESS_OBJECTS
    // -------------------------------------------------------------------------
    GAS_PUMP_MODEL_1 = 3465
    GAS_PUMP_MODEL_2 = 1686
    GAS_PUMP_MODEL_3 = 1676

    // -------------------------------------------------------------------------
    // GTA SA addresses
    // Used by: OilPuddleDrop
    // -------------------------------------------------------------------------
    CWorld__PlayerInFocus = 0xB7CD74 // Cleanup reference: current active player focused by the game

    gFireManager = 0xB71F80
    CFireManager__StartFireAtCoord = 0x539F00
    CFireManager__StartFireOnEntity = 0x53A050

    // -------------------------------------------------------------------------
    // Ignite buffer layout
    // Used by: OilCore, OilPuddleDrop
    // -------------------------------------------------------------------------
    IGNITE_POS_XYZ = 0x00             // OilPuddleDrop reads-clears / OilCore writes external ignition position

    // -------------------------------------------------------------------------
    // oilConfigPtr layout
    // Written by: OilCore
    // Read by: OilPuddleDrop
    // -------------------------------------------------------------------------
    CFG_DROP_IDLE_TIME = 0x00          // Expired state timing
    CFG_DROP_CLEANUP_RADIUS = 0x04     // Cleanup distance from focused active player

    CFG_DROP_INTENSITY = 0x08          // Shadow intensity / fade base
    CFG_SHADOW_RED = 0x0C              // DRAW_SHADOW red
    CFG_SHADOW_GREEN = 0x10            // DRAW_SHADOW green
    CFG_SHADOW_BLUE = 0x14             // DRAW_SHADOW blue

    CFG_FIRE_PROPAGATION_EXTRA = 0x18  // Extra radius for external fire detection
    CFG_FIRE_INTERSECTION_EXTRA = 0x1C // Radius tolerance for burning puddle intersection
    CFG_FIRE_TOUCH_Z_SCALE = 0x20      // Z tolerance scale for fire area check
    CFG_PROPAGATION_DELAY = 0x24       // Delay before creating fire after detection

    CFG_FIRE_BASE_LIFETIME = 0x28      // Written for OilCore lifetime calculation
    CFG_FIRE_LIFETIME_SCALE = 0x2C     // Written for OilCore lifetime calculation
    CFG_FIRE_MAX_LIFETIME = 0x30       // Written for OilCore lifetime calculation

    // -------------------------------------------------------------------------
    // @DROP_DATA layout
    // Written by: OilPuddleDrop
    // Read/updated by: OilCore, OilPuddleDrop
    // -------------------------------------------------------------------------
    DROP_POS_XYZ = 0x00                // Puddle position
    DROP_TARGET_SIZE = 0x0C            // Target visual size, updated by OilCore grow logic
    DROP_FIRE_RADIUS = 0x10            // Ignition/fire radius, updated by OilCore grow logic
    DROP_IS_BURNING = 0x14             // Burning flag, read by OilCore / neighbor puddles
    DROP_FIRE_LIFETIME = 0x18          // Fire lifetime, updated by OilCore

    // -------------------------------------------------------------------------
    // CFire layout
    // Used by: OilPuddleDrop CREATE_OWNED_COORD_FIRE
    // -------------------------------------------------------------------------
    FIRE_TIME_TO_BURN = 0x18           // Patched after native fire creation
    FIRE_STRENGTH = 0x1C               // Patched after native fire creation
END

// Arguments
FLOAT x, y, z
INT charP

FLOAT size, fireRadius
INT fireLifeTotal

INT dropsList, igniteBufferPtr, activeDropsPtr, oilConfigPtr

// Runtime state
INT myPtr, state, firePtr
INT intensity, burnElapsedTime

INT playerInFocus

// Scan state
INT listedDropPtr, listSize
INT scanIndex, listCheckIndex

FLOAT angle
FLOAT targetSize, neighborRadius
FLOAT igniteX, igniteY, igniteZ

// Temp
INT tempInt
FLOAT tempFloat

//***********************************************************************************//

angle = GENERATE_RANDOM_FLOAT_IN_RANGE 0.0 360.0

GET_LABEL_POINTER @DROP_DATA myPtr
WRITE_STRUCT_OFFSET_MULTI myPtr DROP_POS_XYZ 3 4 x y z
WRITE_STRUCT_OFFSET myPtr DROP_TARGET_SIZE 4 size
WRITE_STRUCT_OFFSET myPtr DROP_FIRE_RADIUS 4 fireRadius
WRITE_STRUCT_OFFSET myPtr DROP_IS_BURNING 4 FALSE
WRITE_STRUCT_OFFSET myPtr DROP_FIRE_LIFETIME 4 fireLifeTotal

LIST_ADD dropsList myPtr
UPDATE_ACTIVE_DROPS_COUNT(activeDropsPtr, TRUE)

intensity = READ_STRUCT_OFFSET oilConfigPtr CFG_DROP_INTENSITY 4

state = STATE_IDLE

firePtr = 0

size /= 2.00 // Draw shadow size starts smaller, then interpolates toward target size.

TIMERA = 0 // burn lifetime / idle expire
TIMERB = 0 // detection throttle / propagation delay / heavy burning throttle

//-----------------------------------------------------------------------------------//
///////////////////////////////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------------//

//-----------------------------------------------------------------------------//
:DROP_LOOP
//-----------------------------------------------------------------------------//
WAIT 0

READ_STRUCT_OFFSET myPtr DROP_TARGET_SIZE 4 targetSize
READ_STRUCT_OFFSET myPtr DROP_FIRE_RADIUS 4 fireRadius
READ_STRUCT_OFFSET myPtr DROP_FIRE_LIFETIME 4 fireLifeTotal

GOSUB @check_cleanup
IF state == STATE_CLEANUP
THEN
    JUMP @DROP_EXIT
END

IF size < targetSize
THEN
    size = LERP_FLOAT(size, targetSize, 0.15)
END

GOSUB @apply_fire

IF state == STATE_BURNING
THEN
    // Heavy fire effects are throttled separately from visual fading.
    IF TIMERB > 250
    THEN
        TIMERB = 0
        PROCESS_PEDS(x, y, z, charP, fireRadius)
        PROCESS_OBJECTS(x, y, z, fireRadius)
    END

    burnElapsedTime = TIMERA
    tempInt = READ_STRUCT_OFFSET oilConfigPtr CFG_DROP_INTENSITY 4
    intensity = LERP_INTENSITY(fireLifeTotal, burnElapsedTime, tempInt)
ELSE
    tempInt = READ_STRUCT_OFFSET oilConfigPtr CFG_DROP_IDLE_TIME 4

    IF AND
        state <> STATE_EXPIRED
        TIMERA > tempInt
    THEN
        state = STATE_EXPIRED
        TIMERA = 0
    END

    IF state == STATE_EXPIRED
    THEN
        fireLifeTotal /= 2 // fireLifeTotal is re-read every loop, so this only shortens expired fade timing.
        
        burnElapsedTime = TIMERA
        tempInt = READ_STRUCT_OFFSET oilConfigPtr CFG_DROP_INTENSITY 4
        intensity = LERP_INTENSITY(fireLifeTotal, burnElapsedTime, tempInt)
    END
END

IF intensity > 0
THEN
    DRAW_OIL_SHADOW(x, y, z, angle, size, intensity, oilConfigPtr)
    JUMP @DROP_LOOP
END

//-----------------------------------------------------------------------------//
:DROP_EXIT
//-----------------------------------------------------------------------------//
LIST_REMOVE_VALUE dropsList myPtr
UPDATE_ACTIVE_DROPS_COUNT(activeDropsPtr, FALSE)

TERMINATE_THIS_CUSTOM_SCRIPT

//-----------------------------------------------------------------------------------//
///////////////////////////////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------------//

//_______________________________________________________________
:check_cleanup
//_______________________________________________________________
listCheckIndex += 1
IF listCheckIndex >= 25
THEN
    listCheckIndex = 0

    GOSUB @check_still_listed

    IF state == STATE_CLEANUP
    THEN
        RETURN
    END
END

playerInFocus = READ_MEMORY CWorld__PlayerInFocus 1 FALSE

IF OR
    NOT IS_PLAYER_PLAYING playerInFocus
    NOT IS_PLAYER_CONTROL_ON playerInFocus
THEN
    state = STATE_CLEANUP
    RETURN
END

playerInFocus = GET_PLAYER_CHAR playerInFocus

IF AND
    playerInFocus > 0
    DOES_CHAR_EXIST playerInFocus
THEN
    tempFloat = READ_STRUCT_OFFSET oilConfigPtr CFG_DROP_CLEANUP_RADIUS 4

    IF NOT LOCATE_CHAR_ANY_MEANS_2D playerInFocus x y tempFloat tempFloat FALSE
    THEN
        state = STATE_CLEANUP
        RETURN
    END
ELSE
    state = STATE_CLEANUP
END

RETURN

//_______________________________________________________________
:check_still_listed
//_______________________________________________________________
listSize = GET_LIST_SIZE dropsList

tempInt = 0
WHILE tempInt < listSize
    listedDropPtr = GET_LIST_VALUE_BY_INDEX dropsList tempInt

    IF listedDropPtr == myPtr
    THEN
        RETURN
    END

    tempInt += 1
END

state = STATE_CLEANUP
RETURN

//_______________________________________________________________
:apply_fire
//_______________________________________________________________
IF state < STATE_BURNING
THEN
    IF state == STATE_IDLE
    THEN
        IF TIMERB > 10
        THEN
            TIMERB = 0

            READ_STRUCT_OFFSET_MULTI igniteBufferPtr IGNITE_POS_XYZ 3 4 igniteX igniteY igniteZ

            IF NOT igniteX == 0.0
            THEN
                tempFloat = GET_DISTANCE_BETWEEN_COORDS_3D x y z igniteX igniteY igniteZ

                IF tempFloat < fireRadius
                THEN
                    WRITE_STRUCT_OFFSET_MULTI igniteBufferPtr IGNITE_POS_XYZ 3 4 0.0 0.0 0.0
                    TIMERB = 0
                    state = STATE_PENDING_FIRE
                END
            END

            IF state == STATE_IDLE
            THEN
                GOSUB @check_fire_intersection
            END

            IF state == STATE_IDLE
            THEN
                tempFloat = READ_STRUCT_OFFSET oilConfigPtr CFG_FIRE_PROPAGATION_EXTRA 4
                tempFloat += fireRadius

                tempInt = IS_FIRE_TOUCHING_PUDDLE(x, y, z, tempFloat, oilConfigPtr)

                IF tempInt == TRUE
                THEN
                    TIMERB = 0
                    state = STATE_PENDING_FIRE
                END
            END
        END
    END

    IF state == STATE_PENDING_FIRE
    THEN
        tempInt = READ_STRUCT_OFFSET oilConfigPtr CFG_PROPAGATION_DELAY 4

        IF TIMERB > tempInt
        THEN
            firePtr = CREATE_OWNED_COORD_FIRE(x, y, z, charP, size, fireLifeTotal)

            IF firePtr > 0
            THEN
                state = STATE_BURNING
                WRITE_STRUCT_OFFSET myPtr DROP_IS_BURNING 4 TRUE

                TIMERA = 0
            ELSE
                state = STATE_IDLE
            END
            
            TIMERB = 0
        END
    END
END
RETURN

//_______________________________________________________________
:check_fire_intersection
//_______________________________________________________________
listSize = GET_LIST_SIZE dropsList

IF listSize <= 1
THEN RETURN
END

IF scanIndex >= listSize
THEN scanIndex = 0
END

listedDropPtr = GET_LIST_VALUE_BY_INDEX dropsList scanIndex
scanIndex += 1

IF listedDropPtr == myPtr
THEN RETURN
END

tempInt = READ_STRUCT_OFFSET listedDropPtr DROP_IS_BURNING 4
IF tempInt == FALSE
THEN RETURN
END

READ_STRUCT_OFFSET_MULTI listedDropPtr DROP_POS_XYZ 3 4 igniteX igniteY igniteZ
neighborRadius = READ_STRUCT_OFFSET listedDropPtr DROP_FIRE_RADIUS 4
targetSize = READ_STRUCT_OFFSET oilConfigPtr CFG_FIRE_INTERSECTION_EXTRA 4

tempFloat = GET_DISTANCE_BETWEEN_COORDS_3D x y z igniteX igniteY igniteZ

tempFloat -= fireRadius
tempFloat -= neighborRadius
tempFloat -= targetSize

IF tempFloat < 0.0
THEN
    tempInt = IS_FIRE_TOUCHING_PUDDLE(igniteX, igniteY, igniteZ, neighborRadius, oilConfigPtr)

    IF tempInt == TRUE
    THEN
        TIMERB = 0
        state = STATE_PENDING_FIRE
    END
END

RETURN

//-----------------------------------------------------------------------------------//
///////////////////////////////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------------//

//_______________________________________________________________
FUNCTION UPDATE_ACTIVE_DROPS_COUNT(activeDropsPtr: INT, increment: INT)
    INT count

    count = READ_STRUCT_OFFSET activeDropsPtr 0x00 4

    IF increment == TRUE
    THEN
        count += 1
    ELSE
        count -= 1

        IF count < 0
        THEN
            count = 0
        END
    END

    WRITE_STRUCT_OFFSET activeDropsPtr 0x00 4 count
END

//_______________________________________________________________
FUNCTION DRAW_OIL_SHADOW(posX: FLOAT, posY: FLOAT, posZ: FLOAT, angle: FLOAT, size: FLOAT, intensity: INT, configPtr: INT)
    INT red, green, blue

    red = READ_STRUCT_OFFSET configPtr CFG_SHADOW_RED 4
    green = READ_STRUCT_OFFSET configPtr CFG_SHADOW_GREEN 4
    blue = READ_STRUCT_OFFSET configPtr CFG_SHADOW_BLUE 4

    DRAW_SHADOW 6 posX posY posZ angle size intensity red green blue
END

//_______________________________________________________________
FUNCTION LERP_FLOAT(current: FLOAT, target: FLOAT, factor: FLOAT): FLOAT
    FLOAT result, delta

    delta = target
    delta -= current
    delta *= factor

    result = current
    result += delta

    IF delta > 0.0
    THEN
        IF result > target
        THEN
            result = target
        END
    ELSE
        IF result < target
        THEN
            result = target
        END
    END

    CLEO_RETURN 1 result
END

//_______________________________________________________________
FUNCTION LERP_INTENSITY(totalTime: INT, elapsedTime: INT, baseIntensity: INT): INT
    INT result, remainingTime
    FLOAT value, total, maxIntensity

    IF elapsedTime >= totalTime
    THEN
        CLEO_RETURN 1 0
    END

    remainingTime = totalTime
    remainingTime -= elapsedTime

    maxIntensity =# baseIntensity
    value =# remainingTime
    total =# totalTime

    value /= total
    value *= maxIntensity

    result =# value

    CLEO_RETURN 1 result
END

//_______________________________________________________________
FUNCTION IS_FIRE_TOUCHING_PUDDLE(posX: FLOAT, posY: FLOAT, posZ: FLOAT, radius: FLOAT, configPtr: INT): INT
    INT numFires
    FLOAT minX, minY, minZ
    FLOAT maxX, maxY, maxZ
    FLOAT zTolerance

    zTolerance = READ_STRUCT_OFFSET configPtr CFG_FIRE_TOUCH_Z_SCALE 4
    zTolerance *= radius

    IF zTolerance < 0.15
    THEN
        zTolerance = 0.15
    END

    minX = posX
    minX -= radius

    minY = posY
    minY -= radius

    minZ = posZ
    minZ -= zTolerance

    maxX = posX
    maxX += radius

    maxY = posY
    maxY += radius

    maxZ = posZ
    maxZ += zTolerance

    numFires = GET_NUMBER_OF_FIRES_IN_AREA minX minY minZ maxX maxY maxZ

    IF numFires > 0
    THEN
        CLEO_RETURN 1 TRUE
    END

    CLEO_RETURN 1 FALSE
END

//_______________________________________________________________
FUNCTION CREATE_OWNED_COORD_FIRE(posX: FLOAT, posY: FLOAT, posZ: FLOAT, ownerChar: INT, puddleSize: FLOAT, fireLife: INT): INT
    INT ownerPtr, firePtr
    FLOAT nativeSize

    ownerPtr = 0

    IF ownerChar > -1
    THEN
        ownerPtr = GET_PED_POINTER ownerChar
    END
    
    nativeSize = NORMALIZE_FIRE_SIZE(puddleSize)

    // gFireManager
    // CFireManager::StartFire(CVector position, FLOAT strength, UCHAR unused, CEntity* creator, INT fireLife, CHAR generations, UCHAR unused)
    // CLEO call order: 0 0 fireLife ownerPtr 0 nativeSize posZ posY posX
    firePtr = CALL_METHOD_RETURN CFireManager__StartFireAtCoord gFireManager 9 0 0 0 fireLife ownerPtr 0 nativeSize posZ posY posX
    CLEAR_ALL_SCRIPT_FIRE_FLAGS
    
    IF firePtr > 0
    THEN
        WRITE_STRUCT_OFFSET firePtr FIRE_TIME_TO_BURN 4 fireLife
        WRITE_STRUCT_OFFSET firePtr FIRE_STRENGTH 4 nativeSize
        
        CLEO_RETURN 1 firePtr
    END

    CLEO_RETURN 1 0
END

//_______________________________________________________________
FUNCTION NORMALIZE_FIRE_SIZE(puddleSize: FLOAT): FLOAT
    INT sizeInt
    FLOAT nativeSize, sizeFloat

    nativeSize = puddleSize
    nativeSize *= puddleSize
    nativeSize *= 2.2

    sizeInt =# nativeSize
    sizeFloat =# sizeInt

    IF nativeSize > sizeFloat
    THEN
        sizeInt += 1
    END

    IF sizeInt < 1
    THEN
        sizeInt = 1
    END

    nativeSize =# sizeInt

    CLEO_RETURN 1 nativeSize
END

//_______________________________________________________________
FUNCTION PROCESS_PEDS(posX: FLOAT, posY: FLOAT, posZ: FLOAT, ownerChar: INT, fireRadius: FLOAT)
    INT ped, pedPtr, ownerPtr
    FLOAT searchRadius

    ownerPtr = 0

    IF ownerChar > -1
    THEN
        ownerPtr = GET_PED_POINTER ownerChar
    END

    searchRadius = fireRadius
    searchRadius += 0.25

    IF searchRadius < 1.05
    THEN
        searchRadius = 1.05
    END

    ped = GET_RANDOM_CHAR_IN_SPHERE_NO_SAVE_RECURSIVE posX posY posZ searchRadius FALSE CharSearchFilter.AnyAliveNPC

    WHILE ped <> -1
        IF AND
            DOES_CHAR_EXIST ped
            NOT IS_CHAR_DEAD ped
            NOT IS_CHAR_ON_FIRE ped
            IS_CHAR_ON_FOOT ped
        THEN
            pedPtr = GET_PED_POINTER ped

            // gFireManager
            // CFireManager::StartFire(CEntity* target, CEntity* creator, FLOAT strength, UCHAR unused, INT fireLife, CHAR generations)
            // CLEO call order: 1 10000 0 1.0 ownerPtr pedPtr
            CALL_METHOD CFireManager__StartFireOnEntity gFireManager 6 0 1 10000 0 1.0 ownerPtr pedPtr
        END

        ped = GET_RANDOM_CHAR_IN_SPHERE_NO_SAVE_RECURSIVE posX posY posZ searchRadius TRUE CharSearchFilter.AnyAliveNPC
    END
END

//_______________________________________________________________
FUNCTION PROCESS_OBJECTS(posX: FLOAT, posY: FLOAT, posZ: FLOAT, fireRadius: FLOAT)
    INT obj, objModel, objHealth
    FLOAT objX, objY, objZ, objDist, searchRadius, touchRadius

    searchRadius = fireRadius
    searchRadius += 1.5

    obj = GET_RANDOM_OBJECT_IN_SPHERE_NO_SAVE_RECURSIVE posX posY posZ searchRadius FALSE

    WHILE obj <> -1
        IF DOES_OBJECT_EXIST obj
        THEN
            objModel = GET_OBJECT_MODEL obj

            IF OR
                objModel == GAS_PUMP_MODEL_1
                objModel == GAS_PUMP_MODEL_2
                objModel == GAS_PUMP_MODEL_3
            THEN
                objHealth = GET_OBJECT_HEALTH obj

                IF objHealth > 0
                THEN
                    objX, objY, objZ = GET_OBJECT_COORDINATES obj
                    objDist = GET_DISTANCE_BETWEEN_COORDS_2D posX posY objX objY

                    touchRadius = fireRadius
                    touchRadius += 0.25

                    IF objDist <= touchRadius
                    THEN
                        ADD_EXPLOSION_NO_SOUND objX objY objZ 0
                    END
                END
            END
        END

        obj = GET_RANDOM_OBJECT_IN_SPHERE_NO_SAVE_RECURSIVE posX posY posZ searchRadius TRUE
    END
END

//-----------------------------------------------------------------------------------//
///////////////////////////////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------------//

//=========================================================================//
:DROP_DATA
//=========================================================================//
HEX
    00 (28)
END